#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <cstdlib>
#include <cstring>
#include <strings.h>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

using namespace std;
namespace Server
{

    typedef function<void(int, string, uint16_t, string)> func_t;

    const static string defaultIP = "0.0.0.0";
    enum
    {
        Usage_ERR = 1,
        SOCKET_ERR,
        BIND_ERR,
        OPEN_ERR
    };
    const static int gunm = 1024;
    class udpServer
    {
    public:
        udpServer(const func_t &cb, const uint16_t &port, const string &ip = defaultIP)
            : _callback(cb), _port(port), _ip(ip), _sockfd(-1)
        {
        }
        void initServer()
        {
            // 1、创建套接字
            _sockfd = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET表示ipv4，SOCK_DGRAM表示面向数据报,也就是udp协议,有了前面两个参数，最后一个可以默认为0
            if (_sockfd == -1)
            {
                cerr << "socket error" << errno << ":" << strerror(errno) << endl;
                exit(SOCKET_ERR);
            }
            cout << "socket success : " << _sockfd << endl;

            // 2、绑定ip+port
            // 下面最重要的是bind端口！因为未来该服务的端口号是一直固定的！是一个明确的端口号，不能随意改变！所以需要用户显示bind
            struct sockaddr_in local;      //_in就是_inet，是网络套接字
            bzero(&local, sizeof(local));  // 将内容初始化为0
            local.sin_family = AF_INET;    // 协议家族
            local.sin_port = htons(_port); // 我们双方要都是大端才能通信，所以要进行主机转网络，进行序列转换才能通信
            // ip地址问题 1、ip要从string转换成为uint32_t类型；2、我们ip也是主机序列要转成大端——htonl()——32字节所以是l不是s
            // inet_addr直接完成了上面的两个工作——字符串转整数，主机序列转网络序列
            local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 这里第一次写就用这种方法，因为上面默认bind0.0.0.0
            // ______如果我们上面没有默认0.0.0.0，那么后续我们在服务端输入ip和端口号绑定的时候，只有与该ip进行通信才能被服务端获取，其他ip通信会丢包

            //// 实际ip地址绑定
            //// 我们服务端不需要绑定自己实际的ip地址，只需要绑定0.0.0.0和端口号就行
            //// 因为服务端直接默认bind默认0.0.0.0或者INADDR_ANY，就可以保证任何访问我服务器8080端口的请求全部接收然后交给8080端口对应的进程处理
            //// local.sin_addr.s_addr = htonl(INADDR_ANY);//任意地址bind，服务器的真实写法
            //// 所以，我们以后服务端的ip进行bind都是INADDR_ANY，这样只要是任何访问的端口的服务我都交给对应进程处理，不会出现丢包的情况

            // 开始绑定，上面都是我们用户的操作，操作系统根本不知道，所以需要调用bind函数来绑定
            int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
            if (n == -1)
            {
                cerr << "bind error" << errno << ":" << strerror(errno) << endl;
                exit(BIND_ERR);
            }
        }
        void start()
        {
            // 服务器的本质就是一个死循环——也就是“常驻内存的进程”
            char buffer[gunm]; // 我们自己定义的缓冲区
            while (1)
            {
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer);
                // 这里sizeof(buffer)-1是我们把接收的数据认为是字符串，字符串以'\0'为结束标志
                //(struct sockaddr*)&peer保存是谁发来的数据，传输是数据类型是struct sockaddr*的，所以保存的时候要转换一下
                ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len); // 返回接收到的字节数

                // 1、数据是什么
                // 2、数据是谁发的
                if (s > 0)
                {
                    // peer里面存放了客户端的信息，但是是网络序列的，我们要进行转换
                    buffer[s] = {0};
                    string clientip = inet_ntoa(peer.sin_addr); // 这里不需要.s_addr,只需要struct in_addr结构体
                    uint16_t clientport = ntohs(peer.sin_port);
                    string message = buffer;
                    cout << clientip << "[" << clientport << "]#" << message << endl;
                    _callback(_sockfd, clientip, clientport, message);
                }
            }
        }
        ~udpServer()
        {
        }

    private:
        uint16_t _port;   // 端口号
        string _ip;       // ip地址——实际上，一款网络服务器我们不建议指明一个ip，因为我们可能一个机器有多个网卡，客户端可以通过多个ip地址找到服务端机器，然后找到8080类似的端口
        int _sockfd;      // 文件描述符
        func_t _callback; // 回调
    };

}
